use super::*;

pub(crate) struct Args {
	pub name: Option<Path>,
	pub class: ClassType,
	pub drop: bool,
}
impl parse::Parse for Args {
	fn parse(input: parse::ParseStream) -> Result<Self> {
		let mut name = None;
		let mut class = ClassType::Wrap;
		let mut drop = true;
		let metas = input.parse_terminated(syn::Meta::parse, Token![,])?;
		for meta in metas {
			match meta {
				Meta::NameValue(v) => match (&v.path, &v.value) {
					(n, Expr::Path(v)) if n.is_ident("name") => name = Some(v.path.clone()),
					_ => {}
				},
				Meta::Path(v) if v.is_ident("string") => class = ClassType::String,
				Meta::Path(v) if v.is_ident("number") => class = ClassType::Number,
				Meta::Path(v) if v.is_ident("object") => class = ClassType::Object,
				Meta::Path(v) if v.is_ident("external") => class = ClassType::External,
				Meta::Path(v) if v.is_ident("customdrop") => drop = false,
				_ => {}
			}
		}
		Ok(Args { name, class, drop })
	}
}

pub(crate) fn expand(attr: &Args, input: &mut DeriveInput) -> TokenStream {
	let ty = &input.ident;
	let ts_name = match attr.name.try_ts().unwrap_or_else(|| input.ts()).lit_cstr() {
		Ok(v) => v,
		Err(e) => return e,
	};
	let attrs = &mut input.attrs;
	let drop = match attr.drop {
		true => quote! {impl ::napih::NapiDrop for #ty {}},
		false => quote! {},
	};
	let body = match attr.class {
		ClassType::Number => {
			attrs.push(parse_quote! {#[derive(::napih::derive::IntoNpihNum,::napih::derive::FromNpihNum)]});
			#[cfg(feature = "dts")]
			let dts = build_dts(dts(attr, input));
			#[cfg(not(feature = "dts"))]
			let dts = TokenStream::new();
			quote! {#dts}
		}
		ClassType::String => {
			attrs.push(parse_quote! {#[derive(::napih::derive::IntoNpihStr,::napih::derive::FromNpihStr)]});
			#[cfg(feature = "dts")]
			let dts = build_dts(dts(attr, input));
			#[cfg(not(feature = "dts"))]
			let dts = TokenStream::new();
			quote! {#dts}
		}
		ClassType::Object => {
			attrs.push(parse_quote! {#[derive(::napih::derive::IntoNpihObj,::napih::derive::FromNpihObj)]});
			#[cfg(feature = "dts")]
			let dts = build_dts(dts(attr, input));
			#[cfg(not(feature = "dts"))]
			let dts = TokenStream::new();
			quote! {#dts}
		}
		ClassType::Wrap => {
			attrs.push(parse_quote! {#[derive(::napih::derive::FromNpihRef,::napih::derive::FromNpihMut)]});
			quote! {
				::napih::REG_CLASS!(#ts_name, #ty);
				impl ::napih::NapiClass for #ty {}
				#drop
			}
		}
		ClassType::External => {
			attrs.push(parse_quote! {#[derive(::napih::derive::FromNpihRefExt, ::napih::derive::FromNpihMutExt)]});
			quote! {
				::napih::REG_CLASS!(ext,#ts_name, #ty);
				impl ::napih::NapiExternal for #ty {}
				#drop
			}
		}
	};
	quote! {
		#input
		#body
	}
}
#[cfg(feature = "dts")]
fn dts(attr: &Args, input: &DeriveInput) -> DTSResult {
	let name = attr.name.try_ts().unwrap_or_else(|| input.ts());
	match attr.class {
		ClassType::Number => match &input.data {
			Data::Struct(..) | Data::Union(..) => return Err("struct or union not support for number class"),
			Data::Enum(v) => {
				let mut items = vec![];
				for item in v.variants.iter() {
					let name = item.ident.ts();
					match &item.discriminant {
						Some((_, Expr::Lit(ExprLit { lit: Lit::Int(v), .. }))) => items.push(fomat!("\t"(name)" = "(v))),
						_ => return Err("enum variant must have discriminant"),
					}
				}
				let args = fomat!(for item in items {(item) } separated { ",\n" } );
				Ok(fomat!("export declare const enum "(name)" {\n"(args)"\n}").without(name, TY_ENUM))
			}
		},
		ClassType::String => match &input.data {
			Data::Struct(..) | Data::Union(..) => return Err("struct or union not support for string class"),
			Data::Enum(v) => {
				let mut items = vec![];
				for item in v.variants.iter() {
					let name = item.ident.ts();
					items.push(fomat!("\t"(name)" = \""(name)"\""));
				}
				let args = fomat!(for item in items {(item) } separated { ",\n" } );
				Ok(fomat!("export declare const enum "(name)" {\n"(args)"\n}").without(name, TY_ENUM))
			}
		},
		ClassType::Object => match &input.data {
			Data::Enum(..) | Data::Union(..) => return Err("enum or union not support for object class"),
			Data::Struct(data) => match &data.fields {
				Fields::Named(FieldsNamed { named: fields, .. }) => {
					let mut props = vec![];
					for field in fields {
						let field_name = field.ident.as_ref().unwrap().to_string();
						let field_ty = field.ty.try_ts().ok_or("field type must be ident or ref")?.ts();
						props.push(fomat!("\t"(field_name)": "(field_ty)));
					}
					let props_str = fomat!(for prop in props {(prop) } separated { ";\n" } );
					Ok(fomat!("export interface "(name)" {\n"(props_str)"\n}").without(name, TY_OBJECT))
				}
				Fields::Unnamed(_) => return Err("tuple structs not support for object class"),
				Fields::Unit => Ok(fomat!("export interface "(name)" {}").without(name, TY_OBJECT)),
			},
		},
		ClassType::Wrap => Ok(String::new().without(name, TY_ENUM)),
		ClassType::External => Ok(String::new().without(name, TY_ENUM)),
	}
}
pub(crate) fn expand_impl(input: &mut ItemImpl) -> TokenStream {
	let ty = &input.self_ty;
	for item in input.items.iter_mut() {
		match item {
			ImplItem::Const(ImplItemConst { attrs, .. }) | ImplItem::Fn(ImplItemFn { attrs, .. }) => {
				for attr in attrs.iter_mut().map(|v| &mut v.meta) {
					match attr {
						Meta::List(MetaList { path, tokens, .. }) if path.is_ident("napi_fn") || path.is_ident("napi_const") => {
							tokens.extend(quote! {,owner=#ty})
						}
						Meta::Path(path) if path.is_ident("napi_fn") || path.is_ident("napi_const") => {
							*attr = Meta::List(parse_quote!(#path(owner=#ty)))
						}
						_ => {}
					}
				}
			}
			_ => {}
		}
	}
	quote! {#input}
}
